package com.example.demo.commons;

import com.example.demo.DemoProperties;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;

@Component
public class SsoClient {

    private static final int MAX_RETRY_TIMES = 3;

    private final Logger log = LoggerFactory.getLogger(getClass());

    private final DemoProperties demoProperties;

    @Autowired
    public SsoClient(final DemoProperties demoProperties) {
        this.demoProperties = demoProperties;
    }

    public String getOpenid(final String code) throws IOException {
        final var uri = "/backend/user/user-info";
        final var parameters = new HashMap<String, String>();
        parameters.put("code", code);
        final var resp = post(uri, parameters);
        return resp.get("openid").asText();
    }

    private JsonNode post(final String uri, final Map<String, String> parameters) throws IOException {
        final MessageDigest digest;
        try {
            digest = MessageDigest.getInstance("SM3");
        } catch (NoSuchAlgorithmException e) {
            throw new Error(e.getLocalizedMessage(), e);
        }

        final var reqBody = new ObjectMapper().writeValueAsString(parameters);
        final var reqTimestamp = String.valueOf(System.currentTimeMillis() / 1000);
        final var reqNonce = RandomStringUtils.randomNumeric(10);
        final var reqSignSource = (reqTimestamp + reqNonce + reqBody + demoProperties.getSecret()).getBytes(StandardCharsets.UTF_8);
        final var reqSign = Hex.encodeHexString(digest.digest(reqSignSource));

        final var request = HttpRequest.newBuilder()
                .header("Content-Type", "application/json")
                .header("x-client-code", demoProperties.getClientId())
                .header("x-timestamp", reqTimestamp)
                .header("x-nonce", reqNonce)
                .header("x-sign", reqSign)
                .uri(URI.create(demoProperties.getSsoServiceUrl() + uri))
                .POST(HttpRequest.BodyPublishers.ofString(reqBody))
                .build();
        final var response = tryRequest(request, 0);

        final var resBody = response.body();
        if (resBody == null) {
            throw new IOException("请求后台服务失败：应答为空");
        }

        final var resTimestamp = response.headers().firstValue("x-timestamp");
        final var resSign = response.headers().firstValue("x-sign");
        if (resTimestamp.isPresent() && resSign.isPresent()) {
            final var resSignSource = (resTimestamp.get() + resBody + demoProperties.getSecret()).getBytes(StandardCharsets.UTF_8);
            if (!resSign.get().equalsIgnoreCase(Hex.encodeHexString(digest.digest(resSignSource)))) {
                throw new IOException("请求后台服务失败：应答验签未通过");
            }
        }

        final var objectMapper = new ObjectMapper();
        final var respJson = objectMapper.readTree(resBody);
        final var respCode = respJson.get("respCode").asText();
        if (respCode.startsWith(RespCode.SUCCESS_PREFIX)) { // 请求成功，返回data数据域
            return respJson.get("data");
        } else if (respCode.startsWith(RespCode.ERROR_PREFIX)) { // 请求失败，抛出异常
            final var errMsg = respJson.get("respMsg").asText();
            throw new RemoteCallException(respCode, errMsg);
        } else {
            throw new IOException("请求后台服务失败：异常应答码-" + respCode);
        }
    }

    private HttpResponse<String> tryRequest(final HttpRequest request, final int times) throws IOException {
        try {
            final var requestId = request.hashCode();
            final var urlFuzzy = request.uri().toString().replaceAll("secret=\\w+", "secret=xxxxxxxxxx");
            if (times == 0) {
                log.debug("发送服务请求[{}]：[{}] {}", requestId, request.method(), urlFuzzy);
            } else {
                log.debug("发送服务请求[{}](第{}次尝试)：[{}] {}", requestId, times, request.method(), urlFuzzy);
            }
            final var httpClient = HttpClient.newHttpClient();
            return httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
        } catch (InterruptedException e) {
            if (times < MAX_RETRY_TIMES) {
                return tryRequest(request, times + 1);
            }
            throw new IOException(e.getLocalizedMessage(), e);
        }
    }

    private interface RespCode {
        String SUCCESS_PREFIX = "S";
        String ERROR_PREFIX = "E";
    }

    public static class RemoteCallException extends RuntimeException {

        public final String respCode;

        public RemoteCallException(final String respCode, final String message) {
            super(message + "[" + respCode + "]");
            this.respCode = respCode;
        }
    }
}
